package ckw.database;

import java.util.List;

import javax.annotation.PostConstruct;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import ckw.communication.CommunicationService;
import ckw.database.model.AdministratorToken;
import ckw.database.model.Election;
import ckw.database.model.ElectionChanges;
import ckw.database.model.ElectionNode;
import ckw.database.model.ElectionNonResidentVoter;
import ckw.database.model.ElectionResidentVoter;
import ckw.database.model.Member;
import ckw.database.model.MessageReceived;
import ckw.database.model.MessageToSend;
import ckw.database.model.Node;
import ckw.database.model.NodeChanges;
import ckw.database.model.Vote;
import ckw.database.model.Voter;
import ckw.database.repositories.AdministratorTokenRepository;
import ckw.database.repositories.ElectionChangesRepository;
import ckw.database.repositories.ElectionNodeRepository;
import ckw.database.repositories.ElectionNonResidentVoterRepository;
import ckw.database.repositories.ElectionRepository;
import ckw.database.repositories.ElectionResidentVoterRepository;
import ckw.database.repositories.MemberRepository;
import ckw.database.repositories.MessageReceivedRepository;
import ckw.database.repositories.MessageToSendRepository;
import ckw.database.repositories.NodeChangesRepository;
import ckw.database.repositories.NodeRepository;
import ckw.database.repositories.VoteRepository;
import ckw.database.repositories.VoterRepository;

import com.google.common.collect.Lists;

import core.communication.message.Message;
import core.database.ElectionStatus;
import core.database.NodeType;
import core.exceptions.ObjectDeleteException;
import core.exceptions.ObjectUpdateException;
import core.utils.Converter;
import core.utils.LocatorUriChecker;

@Service
@Transactional
public class DatabaseServiceImpl implements DatabaseService, ApplicationContextAware {

	private static final Logger L = LoggerFactory.getLogger(DatabaseServiceImpl.class);

	private ApplicationContext context;

	@Autowired
	AdministratorTokenRepository administratorTokenRepository;
	@Autowired
	ElectionChangesRepository electionChangesRepository;
	@Autowired
	ElectionNodeRepository electionNodeRepository;
	@Autowired
	ElectionNonResidentVoterRepository electionNonResidentVoterRepository;
	@Autowired
	ElectionRepository electionRepository;
	@Autowired
	ElectionResidentVoterRepository electionResidentVoterRepository;
	@Autowired
	MemberRepository memberRepository;
	@Autowired
	MessageReceivedRepository messageReceivedRepository;
	@Autowired
	MessageToSendRepository messageToSendRepository;
	@Autowired
	NodeRepository nodeRepository;
	@Autowired
	NodeChangesRepository nodesChangeRepository;
	@Autowired
	VoterRepository voterRepository;
	@Autowired
	VoteRepository voteRepository;

	public DatabaseServiceImpl() {
		L.debug("Tworzę DatabaseService");
		L.debug("Utworzono DatabaseService");
	}

	@PostConstruct
	public void setUp() {
		L.debug("setUp");

		Node bNode = nodeRepository.findByNodeType(NodeType.BASENODE);

		if (bNode == null) {
			bNode = new Node("baseNode", "000.0.0.0", "0000", "", NodeType.BASENODE, null);
			bNode = storeNode(bNode);
		}
	}

	@Override
	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
		this.context = applicationContext;
	}

	@Override
	public Node getMainNode() {
		return nodeRepository.findByNodeType(NodeType.CKW);
	}

	@Override
	public Node storeNode(Node node) {
		node = nodeRepository.saveAndFlush(node);

		L.debug("Dodałem nowy węzeł: {}", node);

		return node;
	}

	@Override
	public void deleteNode(Node node) throws ObjectDeleteException {
		L.debug("Usuwam obiekt: {}", node);

		if (node.getNodeType() == NodeType.CKW)
			throw new ObjectDeleteException("Nie można usunąć węzła CKW");

		if (!node.getChildren().isEmpty())
			throw new ObjectDeleteException("Nie można usunąć węzła, który posiada podwęzły");

		if (!node.getElections().isEmpty())
			throw new ObjectDeleteException("Nie można usunąć węzła, który bierze/brał udział w wyborach");

		if (!node.getVoters().isEmpty())
			throw new ObjectDeleteException("Nie można usunąć węzła, który ma przypisanych głosujących");

		if (node.getNodeType() == NodeType.BASENODE)
			throw new ObjectDeleteException("Nie można usunąć węzła");

		deleteNodesChange(node.getLocatorURI());
		node.removeFromParent();
		nodeRepository.delete(node);
	}

	private void deleteNodesChange(String uri) {
		NodeChanges node = nodesChangeRepository.findByUri(uri);
		if (node != null)
			nodesChangeRepository.delete(node);
	}

	@Override
	public void deleteNodeChanges(NodeChanges change) {
		L.debug("Usuwam obiekt: {}", change);
		nodesChangeRepository.delete(change);
	}

	@Override
	public Node getBaseNode() {
		Node node = nodeRepository.findByNodeType(NodeType.BASENODE);
		return node;
	}

	@Override
	public List<MessageToSend> getMessagesToSend() {
		L.debug("Pobieram messagesToSend");

		return messageToSendRepository.findAll();
	}

	@Override
	public List<MessageReceived> getMessagesReceived() {
		L.debug("Pobieram messagesReceived");

		return messageReceivedRepository.findAllByIsHandledFalseOrderByCreatedAsc();
	}

	@Override
	public void removeMessageToSend(Long messageToSendDatabaseId) {
		L.debug("Usuwam messagesToSend o id: {}", messageToSendDatabaseId);

		messageToSendRepository.delete(messageToSendDatabaseId);
	}

	@Override
	public void storeReceivedMessage(Message message) {
		L.debug("saveReceivedMessage {}", message);

		MessageReceived messageR = new MessageReceived();
		messageR.setValue(Converter.objectToXML(message));
		messageR.setType(message.getMessageType());
		messageR = messageReceivedRepository.saveAndFlush(messageR);
		message.setMessageToSendDatabaseId(messageR.getId());
	}

	@Override
	public void setMessageAsHandled(Long id) {
		MessageReceived message = messageReceivedRepository.findOne(id);
		message.setIsHandled(true);
		L.debug("Zapisuje obsłużenie wiadomości {}", message);
		messageReceivedRepository.saveAndFlush(message);
	}

	@Override
	public void storeMessageToSend(Message message) {
		L.debug("saveMessageToSend {}", message);

		MessageToSend messageTS = new MessageToSend();
		messageTS.setValue(Converter.objectToXML(message));
		messageTS.setType(message.getMessageType());
		messageTS = messageToSendRepository.saveAndFlush(messageTS);
		message.setMessageToSendDatabaseId(messageTS.getId());
	}

	@Override
	public NodeChanges storeNodeChanges(NodeChanges nodesChange) {
		nodesChange.onPersist();
		return nodesChangeRepository.saveAndFlush(nodesChange);
	}

	@Override
	public NodeChanges getNodeChanges(String locatorURI) {
		return nodesChangeRepository.findByUri(locatorURI);
	}

	@Override
	public Node getNode(Long id) {
		return nodeRepository.findOne(id);
	}

	@Override
	public List<NodeChanges> getAllNodeChanges() {
		return nodesChangeRepository.findAll();
	}

	@Override
	public List<Node> getAllNodesByType(NodeType type) {
		return nodeRepository.findAllByNodeType(type);
	}

	@Override
	public Boolean isMemberIdentificationFieldTaken(String identificationField) {
		return memberRepository.findByIdentificationField(identificationField) != null;
	}

	@Override
	public Boolean isNodeLocatorUriTaken(String ip, String port) {
		return nodeRepository.findByIpAndPort(ip, port) != null;
	}

	@Override
	public List<Member> getAllMembers() {
		return memberRepository.findAll(new Sort(Direction.ASC, "surname"));
	}

	@Override
	public Member storeMember(Member member) throws ObjectUpdateException {
		boolean update = false;
		String oldIdentificationField = null;

		if (member.getId() != null) {
			update = true;
			oldIdentificationField = member.getIdentificationField();
			Member dbMember = memberRepository.findOne(member.getId());
			if (!member.getIdentificationField().equals(dbMember.getIdentificationField()) && !member.getTokens().isEmpty())
				throw new ObjectUpdateException("Nie można edytować identificationField Membera, który brał/bierze udział w wyborach");

			for (AdministratorToken token : member.getTokens()) {
				if (token.getElectionNode().getElection().getStatus() == ElectionStatus.INPROGRESS)
					throw new ObjectUpdateException("Nie można edytować Membera, który aktualnie bierze udział w wyborach");
			}
		}

		member = memberRepository.saveAndFlush(member);

		L.debug("Dodałem nowego membera: {}", member);

		if (update)
			context.getBean(CommunicationService.class).sendMemberUpdate(oldIdentificationField, member);

		return member;
	}

	@Override
	public void deleteMember(Member member) throws ObjectDeleteException {
		if (member.getTokens().isEmpty())
			memberRepository.delete(member);
		else
			throw new ObjectDeleteException("Nie można usunąć Membera, posiada tokeny");
	}

	@Override
	public Election storeElection(Election election) {
		return electionRepository.saveAndFlush(election);
	}

	@Override
	public void deleteElection(Election election) throws ObjectDeleteException {
		if (election.getStatus() == ElectionStatus.UNSEND || election.getStatus() == ElectionStatus.SEND)
			electionRepository.delete(election);
		else
			throw new ObjectDeleteException("Nie można usunąć wyborów już rozpoczętych/zakończonych");
	}

	@Override
	public List<Election> getAllElections() {
		return electionRepository.findAll(new Sort(Direction.ASC, "name"));
	}

	@Override
	public Voter storeVoter(Voter voter) throws ObjectUpdateException {
		boolean update = false;
		String oldIdentificationField = null;

		if (voter.getId() != null) {
			update = true;
			oldIdentificationField = voter.getIdentificationField();
			Voter dbVoter = voterRepository.findOne(voter.getId());
			if (!voter.getIdentificationField().equals(dbVoter.getIdentificationField()))
				if (!voter.getElections().isEmpty() || !voter.getNonResidentElections().isEmpty())
					throw new ObjectUpdateException("Nie można edytować identificationField Votera, który brał/bierze udział w wyborach");

			for (ElectionNonResidentVoter nrVoter : voter.getNonResidentElections())
				if (nrVoter.getElection().getStatus() == ElectionStatus.INPROGRESS)
					throw new ObjectUpdateException("Nie można edytować Votera, który aktualnie bierze udział w wyborach");

			for (ElectionResidentVoter rVoter : voter.getElections())
				if (rVoter.getElectionNode().getElection().getStatus() == ElectionStatus.INPROGRESS)
					throw new ObjectUpdateException("Nie można edytować Votera, który aktualnie bierze udział w wyborach");
		}

		voter = voterRepository.saveAndFlush(voter);

		if (update)
			context.getBean(CommunicationService.class).sendVoterUpdate(oldIdentificationField, voter);

		return voter;
	}

	@Override
	public Boolean isVoterIdentificationFieldTaken(String identificationField) {
		return voterRepository.findByIdentificationField(identificationField) != null;
	}

	@Override
	public void deleteVoter(Voter voter) throws ObjectDeleteException {
		if (voter.getElections().isEmpty() && voter.getNonResidentElections().isEmpty()) {
			voter.removeFromNode();
			voterRepository.delete(voter);
		} else
			throw new ObjectDeleteException("Nie można usunąć Votera, który brał/bierze udział w wyborach");
	}

	@Override
	public List<Voter> getAllVoters() {
		return voterRepository.findAll(new Sort(Direction.ASC, "surname"));
	}

	@Override
	public ElectionResidentVoter findResidentVoterByVoterAndElectionNode(Voter voter, ElectionNode electionNode) {
		return electionResidentVoterRepository.findByVoterAndElectionNode(voter, electionNode);
	}

	@Override
	public AdministratorToken findAdministratorTokenByMemberAndElectionNode(Member member, ElectionNode electionNode) {
		return administratorTokenRepository.findByMemberAndElectionNode(member, electionNode);
	}

	@Override
	public List<Voter> findVotersLike(String text) {
		text = text.toLowerCase();
		text = "%" + text + "%";

		return voterRepository.findAllLike(text);
	}

	@Override
	public Voter getVoterByIdentificationField(String identificationField) {
		return voterRepository.findByIdentificationField(identificationField);
	}

	@Override
	public ElectionNode findElectionNodeByElectionAndNode(Election election, Node node) {
		return electionNodeRepository.findByElectionAndNode(election, node);
	}

	@Override
	public ElectionNonResidentVoter findNonResidentVoterByVoterAndElection(Voter voter, Election election) {
		return electionNonResidentVoterRepository.findByVoterAndElection(voter, election);
	}

	@Override
	public Election getElection(Long id) {
		return electionRepository.findOne(id);
	}

	@Override
	public ElectionChanges getElectionChanges(String nodeUri, Long electionIdentificationField) {
		return electionChangesRepository.findByUriAndElectionIdentificationField(nodeUri, electionIdentificationField);
	}

	@Override
	public ElectionChanges storeElectionChanges(ElectionChanges electionChanges) {
		electionChanges.onPersist();
		return electionChangesRepository.saveAndFlush(electionChanges);
	}

	@Override
	public List<ElectionChanges> getAllElectionChages() {
		return electionChangesRepository.findAll();
	}

	@Override
	public void deleteElectionChanges(ElectionChanges changes) {
		electionChangesRepository.delete(changes);
	}

	@Override
	public List<ElectionChanges> getAllElectionChages(Long electionIdentificationField) {
		return electionChangesRepository.findAllByElectionIdentificationField(electionIdentificationField);
	}

	@Override
	public List<Node> getAllNodesByLevel(int level) {
		return nodeRepository.findAllByLevel(level);
	}

	@Override
	public Node getNode(String locatorUri) {
		return nodeRepository.findByIpAndPort(LocatorUriChecker.getIp(locatorUri), LocatorUriChecker.getPort(locatorUri));
	}

	@Override
	public AdministratorToken storeAdministratorToken(AdministratorToken token) {
		return administratorTokenRepository.saveAndFlush(token);
	}

	@Override
	public Boolean verifyVoter(Long electionIdentificationField, String voterIdentificationField) {
		ElectionNonResidentVoter voter = electionNonResidentVoterRepository.findByElectionIdAndVoterId(electionIdentificationField, voterIdentificationField);
		if (voter == null)
			return false;

		if (voter.getHasVoted() == true)
			return false;
		else {
			voter.setHasVoted(true);
			voter = electionNonResidentVoterRepository.saveAndFlush(voter);
			return true;
		}
	}

	@Override
	public ElectionNode changeElectionNodeStatus(Long electionId, ElectionStatus status, String locatorURI) {
		Election e = electionRepository.findByIdentificationField(electionId);
		Node n = getNode(locatorURI);

		if (e != null && n != null) {
			ElectionNode en = findElectionNodeByElectionAndNode(e, n);
			en.setStatus(status);
			en = electionNodeRepository.saveAndFlush(en);
			return en;
		}
		return null;
	}

	@Override
	public List<Vote> saveVotes(Long electionIdentificationField, List<Vote> votes) {
		Election election = electionRepository.findByIdentificationField(electionIdentificationField);
		if (election != null) {
			for (Vote vote : votes)
				vote.setElection(election);
			election.getVotes().addAll(votes);
			return voteRepository.save(votes);
		} else
			return Lists.newArrayList();
	}

	@Override
	public Election getElectionByIdentificationField(Long electionIdentificationField) {
		return electionRepository.findByIdentificationField(electionIdentificationField);
	}

	@Override
	public void updateVotes(List<Vote> votes) {
		voteRepository.save(votes);
	}

	@Override
	public List<Vote> getUnparsedVotes() {
		return voteRepository.findAllByParsed(false);
	}

	@Override
	public void updateElectionResidentVoter(ElectionResidentVoter voter) {
		electionResidentVoterRepository.save(voter);
	}

	@Override
	public ElectionResidentVoter getResidentVoterByVoterIdentificationFieldAndElectionIdentificationField(String vId, Long electionIdentificationField) {
		return electionResidentVoterRepository.getResidentVoterByVoterIdentificationFieldAndElectionIdentificationField(vId, electionIdentificationField);
	}
}
